<?php
/**
 * Copyright 2010 Dave Keen
 * http://www.actionscriptdeveloper.co.uk
 * 
 * This file is part of Flextrine.
 * 
 * Flextrine is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * and the Lesser GNU General Public License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 * 
 */

namespace Flextrine;

use Doctrine\Common\ClassLoader,
	Doctrine\Common\Cache\ApcCache,
    Doctrine\ORM\Configuration,
    Doctrine\ORM\EntityManager,
	Doctrine\ORM\Events,
	Doctrine\ORM\Mapping\Driver\AnnotationDriver,
	Doctrine\Common\Annotations\AnnotationReader,
	Flextrine\Tools\AssociationWalker,
	Flextrine\Tools\FlushExecutor,
	Flextrine\Tools\AS3EntityGenerator;

abstract class AbstractFlextrineService {

	protected $em;
	
	protected $flextrineManagerEnabled = true;
	
	const EAGER = "eager";
	const LAZY = "lazy";
	
	function __construct($directory, $connectionOptions) {
		// Set up caches
		$config = new Configuration();
		$cache = new ApcCache();
		//$config->setMetadataCacheImpl($cache);
		//$config->setQueryCacheImpl($cache);

		// The concrete service passes in its home directory so we can construct the actual Proxy directory for this application using that
		$config->setProxyDir($directory.'/../Proxies');
		$config->setProxyNamespace('Proxies');
		$config->setAutoGenerateProxyClasses(true);
		
		$reader = new AnnotationReader();
        $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\');
        $driverImpl = new AnnotationDriver($reader, array($directory."/../entities"));
        $config->setMetadataDriverImpl($driverImpl);
		
		// Create EntityManager
		$this->em = EntityManager::create($connectionOptions, $config);
	}
	
	/**
	 * At present I need to call em->clear() before each request, because if AMFPHP queues up multiple requests at once for some reason having something
	 * in the em breaks the next request's class mapping.
	 */
	
	public function load($entityClass, $id, $fetchMode) {
		$this->em->clear();
		
		if ($fetchMode == self::EAGER) {
			return AssociationWalker::loadProxies($this->em->getRepository($entityClass)->find($id), $this->em);
		} else if ($fetchMode == self::LAZY) {
			return AssociationWalker::insertProxyIdentifiers($this->em->getRepository($entityClass)->find($id), $this->em);
		}
	}
	
	public function loadAll($entityClass, $fetchMode) {
		$this->em->clear();
		
		if ($fetchMode == self::EAGER) {
			return AssociationWalker::loadProxies($this->em->getRepository($entityClass)->findAll(), $this->em);
		} else if ($fetchMode == self::LAZY) {
			return AssociationWalker::insertProxyIdentifiers($this->em->getRepository($entityClass)->findAll(), $this->em);
		}
	}
	
	public function loadBy($entityClass, $criteria, $fetchMode) {
		$this->em->clear();
		
		if ($fetchMode == self::EAGER) {
			return AssociationWalker::loadProxies($this->em->getRepository($entityClass)->findBy($criteria), $this->em);
		} else if ($fetchMode == self::LAZY) {
			return AssociationWalker::insertProxyIdentifiers($this->em->getRepository($entityClass)->findBy($criteria), $this->em);
		}
	}
	
	public function loadOneBy($entityClass, $criteria, $fetchMode) {
		$this->em->clear();
		
		if ($fetchMode == self::EAGER) {
			return AssociationWalker::loadProxies($this->em->getRepository($entityClass)->findOneBy($criteria), $this->em);
		} else if ($fetchMode == self::LAZY) {
			return AssociationWalker::insertProxyIdentifiers($this->em->getRepository($entityClass)->findOneBy($criteria), $this->em);
		}
	}
	
	public function select($query, $fetchMode) {
		if (substr(strtoupper($query), 0, 7) != "SELECT ")
			throw new \Exception("You may only use the SELECT operator");
		
		$this->em->clear();
		
		$result = $this->em->createQuery($query)->getResult();
		
		if ($fetchMode == self::EAGER) {
			return AssociationWalker::loadProxies($result, $this->em);
		} else if ($fetchMode == self::LAZY) {
			return AssociationWalker::insertProxyIdentifiers($result, $this->em);
		}
	}
	
	public function selectOne($query, $fetchMode) {
		if (substr(strtoupper($query), 0, 7) != "SELECT ")
			throw new \Exception("You may only use the SELECT operator");
		
		$this->em->clear();
		
		$result = $this->em->createQuery($query)->getResult();
		
		if ($fetchMode == self::EAGER) {
			return AssociationWalker::loadProxies($result[0], $this->em);
		} else if ($fetchMode == self::LAZY) {
			return AssociationWalker::insertProxyIdentifiers($result[0], $this->em);
		}
	}
	
	public function flush($remoteOperations, $fetchMode) {
		$this->em->clear();
		
		// Execute any beforeFlush custom operations
		foreach ($remoteOperations["beforeFlushCustoms"] as $remoteOperation) {
		//foreach ($remoteOperations->beforeFlushCustoms as $remoteOperation) {
			$remoteOperation = (object)$remoteOperation;
			$data = (object)$remoteOperation->data;
			$this->runCustomOperation($remoteOperation->operation, $data);
		}
		
		// Perform the flush
		$flushExecutor = new FlushExecutor($this->em, $remoteOperations);
		$changeSets = $flushExecutor->flush();
		
		// Go through the elements in the change sets making the entities Flextrine ready (apart from temporaryUidMap which is a special array of strings)
		foreach ($changeSets as $changeSetType => $changeSet) {
			if ($changeSetType != "temporaryUidMap") {
				foreach ($changeSet as $oid => $entity) {
					if ($fetchMode == self::EAGER) {
						AssociationWalker::loadProxies($entity, $this->em);
					} else if ($fetchMode == self::LAZY) {
						AssociationWalker::insertProxyIdentifiers($entity, $this->em);
					}
				}
			}
		}
		
		// Execute any afterFlush custom operations
		foreach ($remoteOperations["afterFlushCustoms"] as $remoteOperation) {
		//foreach ($remoteOperations->afterFlushCustoms as $remoteOperation) {
			$remoteOperation = (object)$remoteOperation;
			$data = (object)$remoteOperation->data;
			$this->runCustomOperation($remoteOperation->operation, $data);
		}
		
		// Return the change sets so they can be replicated in Flextrine
		return $changeSets;
	}
	
	protected function runCustomOperation($operation, $data) {
		
	}
	
	/**
	 * The following functions are for use by the Flextrine Manager.  They are only accessible if $flextrineManagerEnabled is true.
	 */
	public function getFlextrineManagerData() {
		if (!\Config::$flextrineManagerEnabled)
			throw new \Exception("Flextrine manager access is not enabled");
			
		return array("dbname" => $this->em->getConnection()->getDatabase(),
					 "entities" => $this->getSchemaEntities());
	}
	
	public function generateAS3Entities() {
		if (!\Config::$flextrineManagerEnabled)
			throw new \Exception("Flextrine manager access is not enabled");
		
		$as3EntityGenerator = new AS3EntityGenerator($this->getEntityClassesMetaData());
		return $as3EntityGenerator->generate();
	}
	
	public function createSchema() {
		if (!\Config::$flextrineManagerEnabled)
			throw new \Exception("Flextrine manager access is not enabled");
			
		$tool = new \Doctrine\ORM\Tools\SchemaTool($this->em);
		$tool->createSchema($this->getEntityClassesMetaData());
	}
	
	public function updateSchema() {
		if (!\Config::$flextrineManagerEnabled)
			throw new \Exception("Flextrine manager access is not enabled");
			
		$tool = new \Doctrine\ORM\Tools\SchemaTool($this->em);
		$tool->updateSchema($this->getEntityClassesMetaData());
	}
	
	public function dropSchema() {
		if (!\Config::$flextrineManagerEnabled)
			throw new \Exception("Flextrine manager access is not enabled");
			
		$tool = new \Doctrine\ORM\Tools\SchemaTool($this->em);
		$tool->dropSchema($this->getEntityClassesMetaData());
	}
	
	/**
	 * This function must be overridden in the concrete service class to return an array of strings with the fully qualified class of each entity.
	 */
	protected function getSchemaEntities($entitiesPath) {
		return $this->getEntitiesFromPath($entitiesPath);
	}
	 
	private function getEntityClassesMetaData() {
		$metadata = array();
		foreach ($this->getSchemaEntities() as $class)
			$metadata[] = $this->em->getClassMetadata($class);
			
		return $metadata;
	}
	
	private function getEntitiesFromPath($path) {
		$entities = array();
		
		$dir_iterator = new \RecursiveDirectoryIterator($path);
		$iterator = new \RecursiveIteratorIterator($dir_iterator, \RecursiveIteratorIterator::SELF_FIRST);
		
		foreach ($iterator as $file) {
			if ($file->isFile()) {
				$classPath = str_replace("/", "\\", substr($file, strlen($path) + 1, strlen($file) - strlen($path) - 5));
				
				if (!strstr($classPath, "."))
					$entities[] = $classPath;
			}
		}
		
		return $entities;
	}
	
}